using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Timers;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Model.Runtime;
using log4net;
using Kaleida.ServiceMonitor.Framework;

namespace Kaleida.ServiceMonitor.Model
{
    public class Monitor : IDisposable
    {
        private static readonly ILog log = LogManager.GetLogger(typeof (Monitor));
        private static readonly ILog resultsLog = LogManager.GetLogger("ResultsLog");

        private readonly ServiceMonApplication application;
        private readonly CompiledScriptModule compiledModule;
        private readonly MonitorState state;
        
        private readonly OperationStatisticsMap statistics = new OperationStatisticsMap();

        private readonly object responseProcessingLock = new object();
        private readonly object operationSelectionLock = new object();
        private readonly Timer pollTimer = new Timer();
        private readonly Guid id;

        private readonly IList<IServiceMonExtension> extensions;
        readonly ISet<object> naughtyList = new HashSet<object>();

        public Monitor([NotNull] ServiceMonApplication application, [NotNull] CompiledScriptModule compiledModule)
        {
			if (application == null) throw new ArgumentNullException("application");
            if (compiledModule == null) throw new ArgumentNullException("compiledModule");
            
            this.application = application;
            this.compiledModule = compiledModule;

			state = new MonitorState(this, application.Configuration.MaximumResponses);

            this.compiledModule.PollIntervalChanged += (s, e) => UpdateTimerIntervalFromModule();

            extensions = InitialiseExtensions();

            pollTimer.Elapsed += PollTimerTick;
            state.MonitorStarted += (s, e) =>
                                        {
                                            pollTimer.Enabled = true;
                                            UpdateTimerIntervalFromModule();
                                            statistics.Start();
                                        };

            state.MonitorStopped += (s, e) =>
                                        {
                                            pollTimer.Enabled = false;
                                        };

            id = Guid.NewGuid();
            log.DebugFormat("Created monitor process {0} for script module {1} ({2})", id, compiledModule.Name, compiledModule.Id);
        }

        public Guid Id
        {
            get { return id; }
        }

        private void UpdateTimerIntervalFromModule()
        {
            pollTimer.Interval = compiledModule.PollInterval.ToTimeSpan().TotalMilliseconds;
        }

        private void PollTimerTick(object sender, EventArgs e)
        {
            InvokeNextOperation();
        }

        private IList<IServiceMonExtension> InitialiseExtensions()
        {
            return new List<IServiceMonExtension>
                       {
                           new ThresholdChangeEmailer(this),
                           new ScheduledSummaryEmailer(this)
                       };
        }

        public bool HasMisbehavingExtensions
        {
            get { return naughtyList.Any(); }
        }

        public void SendMessageToExtensions(Action<IServiceMonExtension> message)
        {
            foreach (var extension in extensions)
            {
                if (naughtyList.Contains(extension)) continue;

                try
                {
                    message(extension);
                }
                catch (Exception exception)
                {
                    naughtyList.Add(extension);
                    log.Error(string.Format("Extension '{0}' threw an unhandled exception: {1} in monitor {2}", extension.GetType().Name, exception, id));
                }
            }
        }

        [NotNull]
        public ServiceMonApplication Application
        {
            get { return application; }
        }

        [NotNull]
        public CompiledScriptModule CompiledModule
        {
            get { return compiledModule; }
        }

        public MonitorState State
        {
            get { return state; }
        }

        public OperationStatisticsMap Statistics
        {
            get { return statistics; }
        }

        public void InvokeNextOperation()
        {
			if (!state.IsMonitoring || !compiledModule.Operations.Any())
				return;

			var operation = GetNextOperation();
			log.DebugFormat("Monitor '{0}' Calling operation {1} in module {2} ({3})", id, operation.Description, compiledModule.Name, compiledModule.Id);

			var sentOn = DateTime.Now;

			try
			{
				var sw = Stopwatch.StartNew();
				var response = operation.Invoke();
				sw.Stop();

				string responseSummary = operation.ProcessResponse(response) ?? "";
				var result = new RequestSuccessResult(operation, sentOn, sw.Elapsed, responseSummary);

				OperationCompletedSuccessfully(operation, result);
			}
			catch (Exception exception)
			{
				var errorDateTime = DateTime.Now;
				var result = new RequestErrorResult(operation, sentOn, errorDateTime, exception);
				OperationFailureOccurred(operation, result);
			}
        }

        private IOperation GetNextOperation()
        {
            lock (operationSelectionLock)
            {
                return compiledModule.ExecutionOrder.SelectItem(compiledModule.Operations);
            }
        }

        private void OperationCompletedSuccessfully(IOperation operation, RequestSuccessResult result)
        {
            lock (responseProcessingLock)
            {
                Statistics.IncrementSuccessCount(operation, result.RequestDuration);

				resultsLog.Info(result.ToClipboardItemText());

                State.AddResult(result);

                SendMessageToExtensions(i => i.OnOperationSuccessful(new OperationSuccessfulEventArgs(operation, result)));
            }
        }

        private void OperationFailureOccurred(IOperation operation, RequestErrorResult result)
        {
            lock (responseProcessingLock)
            {
                Statistics.IncrementFailureCount(operation);

				resultsLog.Info(result.ToClipboardItemText());

				State.AddResult(result);

                SendMessageToExtensions(i => i.OnOperationFailed(new OperationFailedEventArgs(operation, result)));
            }
        }

        public void AcknowledgeErrors()
        {
            lock (responseProcessingLock)
            {
                State.ClearFailures();

                SendMessageToExtensions(i => i.OnErrorsAcknowledged());
            }
        }

        public void Dispose()
        {
            log.DebugFormat("Disposing Monitor {0}", id);
            pollTimer.Stop();
            pollTimer.Dispose();
            log.DebugFormat("Disposed Monitor {0}", id);
        }
    }
}
